Vue nextTick 官方文档 (opens new window)

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

nextTick 函数的核心逻辑就是利用宏任务和微任务机制。当前 DOM 更新为宏任务,在调用 nextTick 函数后,会将回调函数添加到回调函数列表(异步队列)中,并将异步队列的函数调用放到微任务(Promise.then、MutationObserver、setImmediate 或 setTimeout)中执行。

# nextTick 函数

var callbacks = []; // 异步队列
var pending = false; // 设置是否等待执行标志

function nextTick (cb, ctx) {
  var _resolve;
  // 将回调函数添加到 nextTick 回调函数列表中
  callbacks.push(function () {
    if (cb) {
      try {
        cb.call(ctx);
      } catch (e) {
        handleError(e, ctx, 'nextTick');
      }
    } else if (_resolve) {
      _resolve(ctx);
    }
  });
  if (!pending) {
    pending = true; // 设置正在等待执行标志
    timerFunc(); // 该函数会将回调函数列表的函数调用放到微任务中执行
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(function (resolve) {
      _resolve = resolve;
    })
  }
}

函数执行逻辑:

  • 将回调函数添加到异步队列中。
  • 如果异步队列还未开始等待执行,则将 pending 设置为 true,表示等待执行,即等待在下次 DOM 更新循环结束之后执行延迟回调。
  • 调用 timerFunc 函数,该函数会将回调函数列表的函数调用放到微任务中执行。

# timerFunc 函数

该函数会将回调函数列表的函数调用放到微任务(Promise.then、MutationObserver、setImmediate 或 setTimeout)中执行。

Vue 在内部对异步队列尝试使用原生的 Promise.thenMutationObserversetImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

# Promise.then 实现

var timerFunc;
var p = Promise.resolve();
timerFunc = function () {
  p.then(flushCallbacks);
  // 在有问题的 UIWebViews 中,Promise.then 并没有完全中断,但它可能会陷入一种奇怪的状态,回调被推入微任务队列但队列没有被刷新,直到浏览器需要做一些其他工作,例如 处理一个计时器。 因此,我们可以通过添加一个空计时器来“强制”刷新微任务队列。
  if (isIOS) { setTimeout(noop); }
};
isUsingMicroTask = true;

# MutationObserver 实现

var counter = 1;
var observer = new MutationObserver(flushCallbacks); // 当文本节点变更后会执行回调 flushCallbacks
var textNode = document.createTextNode(String(counter)); // 创建文本节点
observer.observe(textNode, {
  characterData: true
});
timerFunc = function () {
  counter = (counter + 1) % 2; // 变更节点内容
  textNode.data = String(counter);
};
isUsingMicroTask = true;

MutationObserver (opens new window) 会创建并返回一个新的 MutationObserver 它会在指定的 DOM 发生变化时被调用。上面逻辑通过创建一个文本节点,当调用 timerFunc 函数的时候,则会手动触发文本节点内容的变更,从而触发 flushCallbacks 函数的执行。

# 时间函数实现

// setImmediate
timerFunc = function () {
  setImmediate(flushCallbacks);
};

// setTimeout
timerFunc = function () {
  setTimeout(flushCallbacks, 0);
};

# flushCallbacks 函数

function flushCallbacks () {
  pending = false;
  var copies = callbacks.slice(0);
  callbacks.length = 0;
  for (var i = 0; i < copies.length; i++) {
    copies[i]();
  }
}

上面函数的执行逻辑:

  • 将异步队列等待状态 pending 设置为 false。
  • 遍历异步队列,执行每一个 nextTick 回调函数。